﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MetroWeather.cs" company="LifeInSharePoint">
//   Copyright (c) 2013 LifeInSharePoint. All rights reserved.
// </copyright>
// <summary>
//   Weather Display WebPart
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace LifeInSharePoint.Weather.MetroWeather
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.IO;
    using System.Net;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Web.Caching;
    using System.Web.UI;
    using System.Web.UI.WebControls.WebParts;
    using System.Xml;
    using System.Xml.Linq;
    using System.Xml.XPath;
    using System.Xml.Xsl;

    using Microsoft.SharePoint;
    using Microsoft.SharePoint.WebControls;

    /// <summary>
    /// Weather Display WebPart.
    /// </summary>
    [ToolboxItemAttribute(false)]
    public class MetroWeather : WebPart
    {
        #region Private Constants

        /// <summary>
        /// The jsj query file name.
        /// </summary>
        private const string JsjQueryFileName = "/Style Library/LifeInSharePoint.Weather/js/jquery.latest.js";

        /// <summary>
        /// The jsj query include script key.
        /// </summary>
        private const string JsjQueryIncludeScriptKey = "jQueryIncludeScript";

        /// <summary>
        /// The js file name.
        /// </summary>
        private const string JsFileName = "/Style Library/LifeInSharePoint.Weather/js/Weather.js";

        /// <summary>
        /// The js include script key.
        /// </summary>
        private const string JsIncludeScriptKey = "jsWeatherIncludeScript";

        /// <summary>
        /// The include script format.
        /// </summary>
        private const string IncludeScriptFormat = @"<script language=""{0}"" src=""{1}{2}""></script>";

        #endregion

        #region Private Fields

        /// <summary>
        /// The xml list.
        /// </summary>
        private List<XmlDocument> xmlList;

        /// <summary>
        /// The force update.
        /// </summary>
        private bool forceUpdate;

        #endregion

        #region Public Enums

        /// <summary>
        /// The Speed Measure type.
        /// </summary>
        public enum SpeedMeasureType
        {
            /// <summary>
            /// The MPH.
            /// </summary>
            Mph,

            /// <summary>
            /// The KPH.
            /// </summary>
            Kph,
        }

        /// <summary>
        /// The Temperature Measure type.
        /// </summary>
        public enum TemperatureMeasureType
        {
            /// <summary>
            /// The MPH.
            /// </summary>
            Celsius,

            /// <summary>
            /// The KPH.
            /// </summary>
            Fahrenheit,
        }

        /// <summary>
        /// The display type.
        /// </summary>
        public enum DisplayType
        {
            /// <summary>
            /// The horizontal.
            /// </summary>
            Horizontal,

            /// <summary>
            /// The vertical.
            /// </summary>
            Vertical,

            /// <summary>
            /// The summary horizontal.
            /// </summary>
            SummaryHorizontal,

            /// <summary>
            /// The summary vertical.
            /// </summary>
            SummaryVertical
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the weather location.
        /// </summary>
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(true), System.ComponentModel.Category("LifeInSharePoint Config"),
         WebDisplayName("Weather Location"), WebDescription("Weather Location")]
        public string WeatherLocation { get; set; }

        /// <summary>
        /// Gets or sets the num days.
        /// </summary>
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(true), System.ComponentModel.Category("LifeInSharePoint Config"),
         WebDisplayName("Number of Days forcast (Max 5)"), WebDescription("Number of Days forcast (Max 5)")]
        public int NumDays { get; set; }

        /// <summary>
        /// Gets or sets the api key.
        /// </summary>
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(true), System.ComponentModel.Category("LifeInSharePoint Config"),
         WebDisplayName("API Key"), WebDescription("API Key")]
        public string ApiKey { get; set; }

        /// <summary>
        /// Gets or sets the xslt path.
        /// </summary>
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(true), System.ComponentModel.Category("LifeInSharePoint Config"),
         WebDisplayName("XSLT Path"), WebDescription("XSLT Path")]
        public string XsltPath { get; set; }

        /// <summary>
        /// Gets or sets the weather display type.
        /// </summary>
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(true), System.ComponentModel.Category("LifeInSharePoint Config"),
         WebDisplayName("Display Type"), WebDescription("Display Type")]
        public DisplayType WeatherDisplayType { get; set; }

        /// <summary>
        /// Gets or sets the weather display type.
        /// </summary>
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(true), System.ComponentModel.Category("LifeInSharePoint Config"),
         WebDisplayName("Temperature Display"), WebDescription("Temperature Display")]
        public TemperatureMeasureType WeatherTempType { get; set; }

        /// <summary>
        /// Gets or sets the weather display type.
        /// </summary>
        [Personalizable(PersonalizationScope.Shared), WebBrowsable(true), System.ComponentModel.Category("LifeInSharePoint Config"),
         WebDisplayName("Speed Display"), WebDescription("Speed Display")]
        public SpeedMeasureType WeatherSpeedType { get; set; }
        #endregion

        #region Public Methods

        /// <summary>
        /// The convert xml.
        /// </summary>
        /// <param name="xmlDoc">
        /// The xml doc.
        /// </param>
        /// <param name="xsltPath">
        /// The xslt path.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public string ConvertXml(string xmlDoc, string xsltPath)
        {
            // Required to prevent 401 error
            var resolver = new XmlUrlResolver { Credentials = CredentialCache.DefaultCredentials };
            var settings = new XsltSettings(true, true);

            // End
            var doc = new XPathDocument(new StringReader(xmlDoc));
            var xslt = new XslCompiledTransform();
            xslt.Load(xsltPath, settings, resolver);

            var sw = new StringWriter();
            xslt.Transform(doc, null, sw);
            return sw.ToString();
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// The on init.
        /// </summary>
        /// <param name="e">
        /// The e.
        /// </param>
        protected override void OnInit(EventArgs e)
        {
            this.ChromeType = PartChromeType.None;
            base.OnInit(e);
            this.ExportMode = WebPartExportMode.All;
        }

        /// <summary>
        /// Create Child Controls Protected Method.
        /// </summary>
        protected override void CreateChildControls()
        {
            CssRegistration.Register(SPContext.Current.Site.RootWeb.Url + "/Style Library/LifeInSharePoint.Weather/css/climacons-font.css", true);
            CssRegistration.Register(SPContext.Current.Site.RootWeb.Url + "/Style Library/LifeInSharePoint.Weather/css/Weather.css", true);
            ClientScriptManager cs = Page.ClientScript;
            if (!cs.IsClientScriptBlockRegistered(JsjQueryIncludeScriptKey))
            {
                // Create the client script block.
                string includeScript =
                string.Format(IncludeScriptFormat, "javascript", SPContext.Current.Site.RootWeb.Url, JsjQueryFileName);
                cs.RegisterClientScriptBlock(
                    this.GetType(),
                    JsjQueryIncludeScriptKey,
                    includeScript);
            }

            if (!cs.IsClientScriptBlockRegistered(JsIncludeScriptKey))
            {
                // Create the client script block.
                string includeScript =
                string.Format(IncludeScriptFormat, "javascript", SPContext.Current.Site.RootWeb.Url, JsFileName);
                cs.RegisterClientScriptBlock(
                    this.GetType(),
                    JsIncludeScriptKey,
                    includeScript);
            }

            if (!string.IsNullOrEmpty(this.WeatherLocation) && !string.IsNullOrEmpty(this.WeatherLocation) && !string.IsNullOrEmpty(this.ApiKey))
            {
                try
                {
                    XElement locations = new XElement("Locations");
                    locations.SetAttributeValue("DisplayType", this.WeatherDisplayType);
                    this.xmlList = new List<XmlDocument>();
                    this.xmlList = this.GetLocationXml();
                    for (int i = 0; i < this.xmlList.Count; i++)
                    {
                        XmlDocument weatherXmLdoc = this.xmlList[i];
                        string colour = this.WeatherLocation.Split(';')[i].Split(',')[1];
                        this.RenderWeeksWeather(weatherXmLdoc, colour, locations, this.GetLocationName(this.WeatherLocation.Split(';')[i].Split(',')[0], false));
                    }

                    string transformedXml = this.ConvertXml(locations.ToString(), this.XsltPath);
                    this.Controls.Add(new LiteralControl(transformedXml));
                }
                catch (Exception ex)
                {
                    this.Controls.Add(new LiteralControl("An Error occurred.  " + ex.Message + " | " + ex.StackTrace));
                }
            }
            else
            {
                this.Controls.Add(new LiteralControl("No Params Set.  Please Configure WebPart"));
            }
        }

        #endregion

        #region Static Methods

        /// <summary>
        /// Converts Weather codes to class.
        /// </summary>
        /// <param name="code">
        /// The code.
        /// </param>
        /// <param name="manageDayNight">
        /// The manage day night.
        /// </param>
        /// <returns>
        /// Weather Code.
        /// </returns>
        private static string ConvertWeatherCode(string code, bool manageDayNight)
        {
            string weatherClass;
            switch (code)
            {
                case "119":
                    weatherClass = "cloud";
                    break;
                case "116":
                    weatherClass = "cloud sun";
                    break;
                case "356":
                    weatherClass = "rain";
                    break;
                case "293":
                    weatherClass = "rain sun";
                    break;
                case "353":
                    weatherClass = "showers";
                    break;
                case "299":
                case "302":
                    weatherClass = "showers sun";
                    break;
                case "308":
                case "359":
                    weatherClass = "downpour";
                    break;
                case "305":
                    weatherClass = "downpour sun";
                    break;
                case "176":
                    weatherClass = "drizzle sun";
                    break;
                case "320":
                case "317":
                case "365":
                    weatherClass = "sleet";
                    break;
                case "182":
                case "362":
                    weatherClass = "sleet sun";
                    break;
                case "350":
                case "374":
                case "377":
                    weatherClass = "hail";
                    break;
                case "284":
                case "281":
                case "314":
                    weatherClass = "flurries";
                    break;
                case "266":
                case "185":
                case "311":
                    weatherClass = "flurries sun";
                    break;
                case "338":
                case "335":
                case "332":
                case "329":
                case "371":
                case "227":
                case "395":
                    weatherClass = "snow";
                    break;
                case "323":
                case "326":
                case "179":
                case "368":
                    weatherClass = "snow sun";
                    break;
                case "248":
                case "260":
                    weatherClass = "fog";
                    break;
                case "143":
                    weatherClass = "haze";
                    break;
                case "122":
                    weatherClass = "haze sun";
                    break;
                case "230":
                    weatherClass = "wind";
                    break;
                case "392":
                case "389":
                    weatherClass = "lightning";
                    break;
                case "200":
                    weatherClass = "lightning sun";
                    break;
                case "113":
                    weatherClass = "sun";
                    break;

                default:
                    weatherClass = "cloud";
                    break;
            }

            if (manageDayNight)
            {
                if (DateTime.Now.TimeOfDay.Hours > 19 || DateTime.Now.TimeOfDay.Hours < 6)
                {
                    weatherClass = weatherClass.Replace("sun", "moon");
                }
            }

            return weatherClass;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// The render weeks weather.
        /// </summary>
        /// <param name="weatherXmLdoc">
        /// The weather xm ldoc.
        /// </param>
        /// <param name="colour">
        /// The colour.
        /// </param>
        /// <param name="locations">
        /// The locations.
        /// </param>
        private void RenderWeeksWeather(XmlDocument weatherXmLdoc, string colour, XElement locations, string displayName)
        {
            if (string.IsNullOrEmpty(colour))
            {
                colour = "blue";
            }

            XmlNodeList currentWeather = weatherXmLdoc.SelectNodes("/data/current_condition");
            XmlNodeList list = weatherXmLdoc.SelectNodes("/data/weather");
            string currentWeatherCode = ConvertWeatherCode(this.RetrieveXmlNodeValue(currentWeather[0], "weatherCode"), true);
            string currentWindDirection = this.RetrieveXmlNodeValue(currentWeather[0], "winddir16Point").ToLower();
            string currentWindSpeed = string.Empty;
            if (this.WeatherSpeedType.ToString() == "Mph")
            {
                currentWindSpeed = this.RetrieveXmlNodeValue(currentWeather[0], "windspeedMiles").ToLower() + "mph";
            }
            else if (this.WeatherSpeedType.ToString() == "Kph")
            {
                currentWindSpeed = this.RetrieveXmlNodeValue(currentWeather[0], "windspeedKmph").ToLower() + "km/h";
            }

            string cityName = string.IsNullOrEmpty(displayName) ? this.RetrieveXmlNodeValue(weatherXmLdoc, "/data/request/query") : displayName;
            string currentTemp = string.Empty;
            string currentMaxTemp = string.Empty;
            string currentMinTemp = string.Empty;

            if (this.WeatherTempType.ToString() == "Celsius")
            {
                currentTemp = this.RetrieveXmlNodeValue(currentWeather[0], "temp_C") + "&deg;C";
                currentMaxTemp = "Max: " + this.RetrieveXmlNodeValue(list[0], "tempMaxC") + "&deg;C";
                currentMinTemp = "Min: " + this.RetrieveXmlNodeValue(list[0], "tempMinC") + "&deg;C";
            }
            else if (this.WeatherTempType.ToString() == "Fahrenheit")
            {
                currentTemp = this.RetrieveXmlNodeValue(currentWeather[0], "temp_F") + "&deg;F";
                currentMaxTemp = "Max: " + this.RetrieveXmlNodeValue(list[0], "tempMaxF") + "&deg;F";
                currentMinTemp = "Min: " + this.RetrieveXmlNodeValue(list[0], "tempMinF") + "&deg;F";
            }

            XElement currentLocation = new XElement(
                "Location",
                new XElement("Name", cityName),
                new XElement("Colour", colour.ToLower()),
                new XElement("WeatherCode", currentWeatherCode),
                new XElement("WindDirection", currentWindDirection),
                new XElement("WindSpeed", currentWindSpeed),
                new XElement("CurrentTemp", currentTemp),
                new XElement("CurrentMinTemp", currentMinTemp),
                new XElement("CurrentMaxTemp", currentMaxTemp));

            locations.Add(currentLocation);
            if (list.Count > 0 && this.NumDays > 0)
            {
                XElement forecast = new XElement("Forcast");
                currentLocation.Add(forecast);
                foreach (XmlNode node in list)
                {
                    DateTime currentDate = DateTime.Parse(this.RetrieveXmlNodeValue(node, "date"));
                    string currentCode = ConvertWeatherCode(this.RetrieveXmlNodeValue(node, "weatherCode"), false);
                    string windDirection = this.RetrieveXmlNodeValue(node, "winddirection").ToLower();

                    string windSpeed = string.Empty;
                    if (this.WeatherSpeedType.ToString() == "Mph")
                    {
                        windSpeed = this.RetrieveXmlNodeValue(node, "windspeedMiles").ToLower() + "mph";
                    }
                    else if (this.WeatherSpeedType.ToString() == "Kph")
                    {
                        windSpeed = this.RetrieveXmlNodeValue(node, "windspeedKmph").ToLower() + "km/h";
                    }

                    string maxTemp = string.Empty;
                    if (this.WeatherTempType.ToString() == "Celsius")
                    {
                        maxTemp = this.RetrieveXmlNodeValue(node, "tempMaxC") + "&deg;C";
                    }
                    else if (this.WeatherTempType.ToString() == "Fahrenheit")
                    {
                        maxTemp = this.RetrieveXmlNodeValue(node, "tempMaxF") + "&deg;F";
                    }

                    XElement dayOfWeek = new XElement(
                        "DayOfWeek",
                        new XElement("Day", currentDate.ToString("ddd")),
                        new XElement("WeatherCode", currentCode),
                         new XElement("WindDirection", windDirection),
                         new XElement("WindSpeed", windSpeed),
                        new XElement("MaxTemp", maxTemp));
                    forecast.Add(dayOfWeek);
                }
            }
        }

        /// <summary>
        /// The get location xml.
        /// </summary>
        /// <returns>
        /// The <see cref="List"/>.
        /// </returns>
        private List<XmlDocument> GetLocationXml()
        {
            List<XmlDocument> localList = new List<XmlDocument>();
            this.forceUpdate = !string.IsNullOrEmpty(this.Page.Request.QueryString["Force"]);
            if (HttpRuntime.Cache["WeatherXmlList"] == null || this.forceUpdate == true)
            {
                string[] locations = this.WeatherLocation.Split(';');
                foreach (string location in locations)
                {
                    string locationName = this.GetLocationName(location.Split(',')[0], true);
                    if (!string.IsNullOrEmpty(locationName))
                    {
                        HttpWebRequest weatherRequest =
                            (HttpWebRequest)
                            WebRequest.Create(
                                string.Format(
                                    "http://free.worldweatheronline.com/feed/weather.ashx?q={0}&format=xml&num_of_days={1}&key={2}",
                                    locationName,
                                    this.NumDays,
                                    this.ApiKey));
                        HttpWebResponse weatherResponse = (HttpWebResponse)weatherRequest.GetResponse();
                        XmlDocument weatherXmLdoc = new XmlDocument();
                        weatherXmLdoc.Load(weatherResponse.GetResponseStream());

                        localList.Add(weatherXmLdoc);
                    }
                }

                if (localList.Count > 0)
                {
                    HttpRuntime.Cache.Add(
                        "WeatherXmlList" + this.ID,
                        localList,
                        null,
                    DateTime.Now.AddMinutes(1),
                        Cache.NoSlidingExpiration,
                        CacheItemPriority.Default,
                        null);
                }
            }
            else
            {
                localList = (List<XmlDocument>)HttpRuntime.Cache["WeatherXmlList" + this.ID];
            }

            return localList;
        }

        private string GetLocationName(string location, bool returnLocation)
        {
            string pattern = @"{([^}]+)\}";
            Match match = Regex.Match(location, pattern);
            if (match.Success)
            {
                string key = match.Groups[1].Value;
                if (returnLocation)
                {
                    return location.Replace('{' + key + '}', string.Empty);
                }
                else
                {
                    return key;
                }
            }
            else
            {
                return location;
            }
        }

        /// <summary>
        /// The retrieve xml node value.
        /// </summary>
        /// <param name="node">
        /// The node.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        private string RetrieveXmlNodeValue(XmlNode node, string value)
        {
            string returnVal = string.Empty;
            XmlNode selectSingleNode = node.SelectSingleNode(value);
            if (selectSingleNode != null)
            {
                returnVal = selectSingleNode.InnerText;
            }

            return returnVal;
        }

        #endregion
    }
}